// Copyright (C) 2023 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
// 
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<北京小羊驼科技有限公司>，授权编号：<NT231212144350>，代码指纹：<A.702363430.550>，技术对接人微信：<ID: wxid_wbyootvkdcgj22>】
// 【授权寄送：<收件：苏先生、地址：北京市大兴区北京密码西区6号楼1单元301、电话：18613885610、邮箱：bd@huiyuanxiang-inc.com>】
// 
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
// 
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rainbowchat.http.file.u4web;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.eva.epc.common.file.FileHelper;
import com.eva.epc.common.util.se.ImgEqualScaleAndCutHelper;
import com.eva.framework.utils.EndsExceptionFactory;
import com.eva.framework.utils.LoggerFactory;
import com.google.gson.Gson;
import com.x52im.rainbowchat.BaseConf;
import com.x52im.rainbowchat.http.file.u.UserAvatarUploader;

/**
 * 文件上传接口。
 * <p>
 * 本接口目前主要用于Web端的文件上传，因为Web客户端没有MD5码生成能力
 * ，而APP端是有的，所以Web端文件上传的服务端代码跟APP端是有少许区别
 * 的，暂时就不考虑重用APP端的文件上传接口了。
 * <p>
 * APP端的文件上传，请见：com.x52im.rainbowchat.http.file.u包下的各接口。
 * 
 * @author Jack Jiang
 * @version 1.0
 */
public class FileUploader4Web extends HttpServlet
{
	/** 上传业务类型：图片聊天消息中的图片文件上传 */
	public final static String ACTION_IMAGE_OF_CHAT_MSG_UPLOAD = "0";
	/** 上传业务类型：大文件聊天消息中的普通文件上传 */
	public final static String ACTION_FILE_OF_CHAT_MSG_UPLOAD = "1";
	/** 上传业务类型：用户头像的的图片文件上传 */
	public final static String ACTION_IMAGE_OF_AVATAR_UPLOAD = "2";
	
	/**
	 * <p>
	 * Apache文件上传组件在解析上传数据中的每个字段内容时，需要临时保存解析出的数据，以
	 * 便在后面进行数据的进一步处理（保存在磁盘特定位置或插入数据库）。因为Java虚拟机默
	 * 认可以使用的内存空间是有限的，超出限制时将会抛出“java.lang.OutOfMemoryError”错
	 * 误。如果上传的文件很大，例如800M的文件，在内存中将无法临时保存该文件内容，Apache
	 * 文件上传组件转而采用临时文件来保存这些数据；但如果上传的文件很小，例如600个字节的
	 * 文件，显然将其直接保存在内存中性能会更加好些。
	 * 
	 * <p>
	 * setSizeThreshold方法用于设置是否将上传文件已临时文件的形式保存在磁盘的临界值（以
	 * 字节为单位的int值），如果从没有调用该方法设置此临界值，将会采用系统默认值10KB。对
	 * 应的getSizeThreshold() 方法用来获取此临界值。*/
	private static final int THRESHOLD_SIZE = 1024 * 200;      // 200KB
	private static final int MAX_FILE_SIZE = 1024 * 1024 * 50; // 50MB
	private static final int MAX_REQUEST_SIZE = MAX_FILE_SIZE; // 50MB
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response) 
			throws ServletException, IOException
	{
		doPost(request, response);
	}

	/**
	 * handles file upload via HTTP POST method
	 */
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException
	{
		// 获取浏览器端提交的参数（从URL中）
		String action = request.getParameter("action");    // 业务类型：0-图片上传、1-普通文件上传、2-本地用户头像文件上传
		String userUid = request.getParameter("user_uid"); // 文件上传者用户uid（不参与具体业务，仅为了以后作身份认证或log记录）
					
		try
		{
			boolean needCalculateMD5FileName = true;//(fileName==null);
			
			LoggerFactory.getLog().debug("[HTTP-"+getLogTAG(action)+"]正在处理用户uid="+userUid
					+"的文件上传请求(needCalculateMD5FileName?"+needCalculateMD5FileName+")...................");
			
			// 保存目录
			String destDir = null;
			
			if(isFileOfChatMsg(action))
				destDir = BaseConf.getInstance().getDIR_USER_BIGFILE_UPLOAD();
			else if(isImageOfChatMsg(action))
				destDir = BaseConf.getInstance().getDIR_USER_IMAGE_UPLOAD_UNREAD();
			else if(isFileOfAvatar(action))
				destDir = BaseConf.getInstance().getDIR_USER_AVATAR_UPLOAD();
			
			// 此方法目前是通用的文件上传处理API，默认是支持多文件上传的，目前是假定客户端只传一个文件，服务端其实并没有做检查，实际应用时不会！
			// 本方法处理完成并返回相应的数据集合（可能每种业务返回的数据内容都不一样哦，具体以业务类型为准）
			HashMap<String, String> mapAfterSavedFile = FileUploadImpl.processFileUpload(request, response
					, destDir
					, THRESHOLD_SIZE, MAX_FILE_SIZE, MAX_REQUEST_SIZE
					, needCalculateMD5FileName
					, action);
			
			// 文件上传成功后要做的额外处理逻辑（以无异常地走到本代码，即可表示文件上传成功了）
			afterSavedFile(action, userUid, destDir, mapAfterSavedFile);
			
			// 如有数据需要返回给客户的话
			if(!mapAfterSavedFile.isEmpty())
			{
				// 文件上传成功后，需要将上传完成后保存的最终文件名写回给浏
				// 览器端（以备浏览器端在文件上传完成后进入下一个处理步骤
				PrintWriter writer = response.getWriter();
				// 返回一个JSON对象字符串给web端（web端将继续进行后续处理，务必与web端保持一致！
				// （web端的具体使用详见web端：rhchat_ui_module.js中的initFileUplodifive5函数）
				writer.println(new Gson().toJson(mapAfterSavedFile));
//				writer.println("{\"fileNameMD5\":\""+newFileName+"\"}");
//				writer.println("<script type='text/javascript'>window.parent.testJSONP();</script>");
//				writer.println("<script type='text/javascript'>parent.im_file_upload.g_test1();</script>");
				writer.flush();
			}
		}
//		catch (Exception ex)
//		{
//			LoggerFactory.getLog().error("[HTTP-"+getLogTAG(action)+"]处理Web端上传时出错了，"+ex.getMessage(), ex.getCause());
//			throw new IOException("[HTTP-"+getLogTAG(action)+"]Upload error:"+ex.getMessage(), ex.getCause());
////			request.setAttribute("message", "There was an error: " + ex.getMessage());
//		}

		catch (EndsExceptionFactory.EInvalidTokenException ee)
		{
			String logStr = "处理Web端上传时出错了，原因："+ee.getMessage();
			LoggerFactory.getLog().error("[HTTP"+getLogTAG(action)+"上传] token验证失败了，本次上传被拒绝！", ee);
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			response.getWriter().write(logStr);
		}
		catch (Exception ex)
		{
			String logStr = "处理Web端上传时出错了，原因："+ex.getMessage();
			LoggerFactory.getLog().error("[HTTP"+getLogTAG(action)+"上传] "+logStr, ex);
			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			response.getWriter().write(logStr);
		}
	}

	/**
	 * 文件上传完成后要处理的额外事情。
	 * 
	 * @param action
	 * @param uploadedPath
	 * @param userUid 上传发起者的uid
	 * @param mapAfterSavedFile
	 */
	private void afterSavedFile(String action, String userUid
			, String uploadedPath, HashMap<String, String> mapAfterSavedFile)
	{
		// 图片消息文件上传时：原文件上传和md5重新名完成后，则还需要生成缩略图（以便APP或Web端方便预览）
		if(FileUploader4Web.isImageOfChatMsg(action))
		{
			// 前面步骤里上传完成后以md5码命名的最终文件名
			String finalMD5FileName = mapAfterSavedFile.get("fileNameMD5");

			// 生成原图的缩略图并存到磁盘
			File thumbnailFilePath = new File(uploadedPath + File.separator + "th_" + finalMD5FileName);
			// 如果该md5文件名命名（其实此md5码就是文件数据的指纹）存在，即意味着不需要写入磁盘了
			if(!thumbnailFilePath.exists())
			{
				// 用等比缩放并裁切的算法（此算法自2014-03-29实施），算法将保证缩略图的生成不变形
				try
				{
					ImgEqualScaleAndCutHelper.saveImageAsJpg(
							uploadedPath + File.separator + finalMD5FileName
							, thumbnailFilePath.getAbsolutePath()
							// 生成的缩略图参考微信的按100*100的极限缩放
							, 450, 450// 20190605 ：为了更好的适应高dpi手机，已由原200*200改为450*450
							);
				}
				catch (Exception e)
				{
					LoggerFactory.getLog().warn("[HTTP-"+FileUploader4Web.getLogTAG(action)+"-"+userUid+"] 图片缩略图生成时出错了！", e);
				}
			}
			else
			{
				LoggerFactory.getLog().warn("[HTTP-"+FileUploader4Web.getLogTAG(action)+"-"+userUid+"] 图片缩略图"
						+thumbnailFilePath.getAbsolutePath()+"已经存在，不需要再生成了哦！");
			}
		}
		// 头像上传时：原文件上传和md5重新名完成后，上传完成后，则还需要进行头像的裁切、头像缩略图生成等
		else if(FileUploader4Web.isFileOfAvatar(action))
		{
			// 前面步骤里上传完成后以md5码命名的最终文件名
			String MD5FileName = mapAfterSavedFile.get("fileNameMD5");
			File originalFilePath = new File(uploadedPath + File.separator + MD5FileName);
			
			// 头像文件将要存放的真正位置
			String avatarFileName = getUserAvatarFileName(userUid, MD5FileName);
			File avatarFilePath = new File(uploadedPath + File.separator + avatarFileName);
			
			//**【1】 进行头像图片的标准大小裁剪
			if(!avatarFilePath.exists())
			{
				try
				{
					// 先裁剪生成标准头像
					// （用等比缩放并裁切的算法（此算法自2014-03-29实施），算法将保证图的生成不变形）
					ImgEqualScaleAndCutHelper.saveImageAsJpg(
							// 原图片
							originalFilePath.getAbsolutePath()
							// 裁剪后保存的位置
							, avatarFilePath.getAbsolutePath()
							// 为了匹配APP产品，统一头像大小为640*640（这也是微信的用户头像尺寸）
							, 640, 640 
							);
					
					// 再进行头像缩略图的生成，以及余下处理逻辑
					UserAvatarUploader.afterSavedAvatarFile(userUid, avatarFileName);
				}
				catch (Exception e)
				{
					LoggerFactory.getLog().warn("[HTTP-"+FileUploader4Web.getLogTAG(action)+"-"+userUid+"] 头像图片裁剪时出错了！", e);
				}
			}
			// 如果该图像文件已存在，即意味着不需要写入磁盘了，否则就要开始进行正常处理逻辑
			else
			{
				LoggerFactory.getLog().warn("[HTTP-"+FileUploader4Web.getLogTAG(action)+"-"+userUid+"] 头像图片"
						+avatarFilePath.getAbsolutePath()+"已经存在，不需要再生成了哦！");
			}
			
			//**【2】裁剪处理完成后，删除原始文件（没用了）
			boolean oroginalFileDeleteSUcess = FileHelper.deleteFile(originalFilePath.getAbsolutePath());
			LoggerFactory.getLog().debug("[HTTP-"+FileUploader4Web.getLogTAG(action)+"-"+userUid+"] 头像图片的原始文件："
					+originalFilePath.getAbsolutePath()+" 删除成功了吗？"+oroginalFileDeleteSUcess);
			
			// 注意：这才是返回给浏览器端的真正头像文件名
			mapAfterSavedFile.put("fileNameMD5", avatarFileName);
		}
	}
	
	public static String getLogTAG(String action)
	{
		String tag = "";
		if(isImageOfChatMsg(action))
			tag = "WEB图片消息";
		else if(isFileOfChatMsg(action))
			tag = "WEB文件消息";
		else if(isFileOfAvatar(action))
			tag = "WEB头像图片";
		else
			tag = "UNKOWN";
		
		return tag;
	}
	
	/**
	 * 是否是图片聊天消息中的图片文件上传业务。
	 * 
	 * @param action
	 * @return
	 */
	public static boolean isImageOfChatMsg(String action)
	{
		return ACTION_IMAGE_OF_CHAT_MSG_UPLOAD.equals(action);
	}
	
	/**
	 * 是否是大文件聊天消息中的普通文件上传业务。
	 * 
	 * @param action
	 * @return
	 */
	public static boolean isFileOfChatMsg(String action)
	{
		return ACTION_FILE_OF_CHAT_MSG_UPLOAD.equals(action);
	}
	
	/**
	 * 是否是用户头像的图片文件上传业务。
	 * 
	 * @param action
	 * @return
	 */
	public static boolean isFileOfAvatar(String action)
	{
		return ACTION_IMAGE_OF_AVATAR_UPLOAD.equals(action);
	}
	
	/**
	 * 返回用户头像文件名的完整内容格式（即RainbowChat中用户头像存放于服务端磁盘中的真正文件名）。
	 * 
	 * @param uid
	 * @param md5ForFile
	 * @return
	 */
	public static String getUserAvatarFileName(String uid, String md5ForFile)
	{
		if(uid == null || md5ForFile == null)
		{
			LoggerFactory.getLog().warn(getLogTAG("["+ACTION_IMAGE_OF_AVATAR_UPLOAD)+"]无效的参数：uid == null || md5ForFile == null!");
			return null;
		}
		return uid+"_"+md5ForFile+".jpg";
	}
}
